using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.ComponentModel.Design;
using System.Collections;

namespace needle.Services.astra.Collections
{
	/// <summary>
	/// List of documents.
	/// Manages documents and can open, close or save them etc.
	/// </summary>
	public abstract class Documents : ItemsChangeList<IDocument>, INotifyPropertyChanged
	{
		#region Delegates

		/// <summary>
		/// See <see cref="CreateMethod"/>.
		/// </summary>
		public delegate IDocument CreateDelegate();
		/// <summary>
		/// See <see cref="GetOpenUriMethod"/>.
		/// </summary>
		public delegate IList<Uri> GetOpenUriDelegate(ref bool readOnly);
		/// <summary>
		/// See <see cref="GetSaveUriMethod"/>.
		/// </summary>
		public delegate Uri GetSaveUriDelegate(IDocument document);
		/// <summary>
		/// See <see cref="ShouldReopenMethod"/>.
		/// </summary>
		public delegate bool ShouldReopenDelegate(IDocument document);
		/// <summary>
		/// See <see cref="ShouldSaveChangesBeforeCloseMethod"/>.
		/// </summary>
		public delegate bool? ShouldSaveChangesBeforeCloseDelegate(IList<IDocument> documents);
		/// <summary>
		/// See <see cref="ShouldOverwriteMethod"/>.
		/// </summary>
		public delegate bool? ShouldOverwriteDelegate(IDocument document, Uri uri);
		/// <summary>
		/// See <see cref="ShouldBackupMethod"/>.
		/// </summary>
		public delegate bool ShouldBackupDelegate(Uri uri);

		/// <summary>
		/// See <see cref="DoCreate"/>.
		/// </summary>
		/// <value>
		/// set: If <see cref="Count"/> is 0 and <see cref="CanCreate"/> is true after setting this value, <see cref="Create"/> is called.
		/// </value>
		public CreateDelegate CreateMethod
		{
			get { return _createMethod; }
			set
			{
				_createMethod = value;
				if (this.Count == 0 && this.CanCreate)
					Create();
			}
		}
		/// <summary>
		/// See <see cref="GetOpenUri"/>.
		/// </summary>
		public GetOpenUriDelegate GetOpenUriMethod
		{
			get { return _getOpenUriMethod; }
			set { _getOpenUriMethod = value; }
		}
		/// <summary>
		/// See <see cref="GetSaveUri"/>.
		/// </summary>
		public GetSaveUriDelegate GetSaveUriMethod
		{
			get { return _getSaveUriMethod; }
			set { _getSaveUriMethod = value; }
		}
		/// <summary>
		/// See <see cref="ShouldReopen"/>.
		/// </summary>
		public ShouldReopenDelegate ShouldReopenMethod
		{
			get { return _shouldReopenMethod; }
			set { _shouldReopenMethod = value; }
		}
		/// <summary>
		/// See <see cref="ShouldSaveChangesBeforeClose"/>.
		/// </summary>
		public ShouldSaveChangesBeforeCloseDelegate ShouldSaveChangesBeforeCloseMethod
		{
			get { return _shouldSaveChangesBeforeCloseMethod; }
			set { _shouldSaveChangesBeforeCloseMethod = value; }
		}
		/// <summary>
		/// See <see cref="ShouldOverwrite"/>.
		/// </summary>
		public ShouldOverwriteDelegate ShouldOverwriteMethod
		{
			get { return _shouldOverwriteMethod; }
			set { _shouldOverwriteMethod = value; }
		}
		/// <summary>
		/// See <see cref="ShouldBackup"/>.
		/// </summary>
		public ShouldBackupDelegate ShouldBackupMethod
		{
			get { return _shouldBackupMethod; }
			set { _shouldBackupMethod = value; }
		}

		/// <summary>
		/// See <see cref="CreateMethod"/>.
		/// </summary>
		private CreateDelegate _createMethod;
		/// <summary>
		/// See <see cref="GetOpenUriMethod"/>.
		/// </summary>
		private GetOpenUriDelegate _getOpenUriMethod;
		/// <summary>
		/// See <see cref="GetSaveUriMethod"/>.
		/// </summary>
		private GetSaveUriDelegate _getSaveUriMethod;
		/// <summary>
		/// See <see cref="ShouldReopenMethod"/>.
		/// </summary>
		private ShouldReopenDelegate _shouldReopenMethod;
		/// <summary>
		/// See <see cref="ShouldSaveChangesBeforeCloseMethod"/>.
		/// </summary>
		private ShouldSaveChangesBeforeCloseDelegate _shouldSaveChangesBeforeCloseMethod;
		/// <summary>
		/// See <see cref="ShouldOverwriteMethod"/>.
		/// </summary>
		private ShouldOverwriteDelegate _shouldOverwriteMethod;
		/// <summary>
		/// See <see cref="ShouldBackupMethod"/>.
		/// </summary>
		private ShouldBackupDelegate _shouldBackupMethod;

		#endregion

		/// <summary>
		/// Constructor.
		/// </summary>
		protected Documents()
			: base(null, delegate(IDocument document) { return document != null; })
		{
		}

		/// <summary>
		/// Current document.
		/// </summary>
		/// <remarks>
		/// While changing this list:
		/// When a document is added to this list, it is made current.
		/// If current document is removed from this list, previous document is made current.
		/// Otherwise first document is made current if it exists, otherwise this property is null.
		/// </remarks>
		/// <value>Default: null.</value>
		/// <exception cref="ArgumentOutOfRangeException">set: Value is not null and is not in this list.</exception>
		public IDocument Current
		{
			get { return _current; }
			set
			{
				if (_current == value)
					return;
				if (value != null && !this.Contains(value))
					throw new ArgumentOutOfRangeException("Current", value, "Document is not in this list.");
				_current = value;
				OnPropertyChanged("Current");
				OnPropertyChanged("HasCurrent");
			}
		}
		/// <summary>
		/// Whether <see cref="Current"/> is not null.
		/// </summary>
		public bool HasCurrent
		{
			get { return this.Current != null; }
		}
		/// <summary>
		/// Whether call to <see cref="SaveCurrent"/> makes sense.
		/// </summary>
		/// <value>
		/// <see cref="HasCurrent"/> is true and <see cref="Current"/> has <see cref="IsNew"/> or <see cref="IsChanged"/> true.
		/// </value>
		public bool CanSaveCurrent
		{
			get { return this.HasCurrent && (this.Current.IsNew || this.Current.IsChanged); }
		}
		/// <summary>
		/// Extension added to the end of document uri to prepare target backup uri.
		/// See <see cref="Backup"/>.
		/// </summary>
		/// <value>Default: ".bak".</value>
		/// <exception cref="ArgumentNullException">set: Value is null or empty.</exception>
		public string BackupExtension
		{
			get { return _backupExtension; }
			set
			{
				if (string.IsNullOrEmpty(value))
					throw new ArgumentNullException("BackupExtension");
				if (_backupExtension == value)
					return;
				_backupExtension = value;
				OnPropertyChanged("BackupExtension");
			}
		}
		/// <summary>
		/// Gets a document with <paramref name="uri"/> from this list.
		/// Calls <see cref="Validate(Uri)"/>.
		/// </summary>
		/// <param name="uri">Document uri.</param>
		/// <returns>Document if found, otherwise null.</returns>
		public IDocument this[Uri uri]
		{
			get
			{
				Validate(uri);
				foreach (IDocument document in this)
				{
					if (document.Uri == uri)
						return document;
				}
				return null;
			}
		}

		/// <summary>
		/// Creates new document and adds it to this list.
		/// </summary>
		/// <remarks>Calls <see cref="DoCreate"/>.</remarks>
		public void Create()
		{
			Add(DoCreate());
		}
		/// <summary>
		/// Opens documents.
		/// </summary>
		/// <remarks>
		/// If a document with specified uri is already open, if <see cref="IDocument.IsChanged"/> is true, <see cref="ShouldReopen"/> is called and if it returns true, the open document is discarded (removed from this collection without saving) and open operation continues normally, if <see cref="IDocument.IsChanged"/> is false or <see cref="ShouldReopen"/> returns false, <see cref="Current"/> is set to the open document and next document uri (if available) is processed.
		/// Calls <see cref="Open(Uri, bool)"/> for each non-null uri.
		/// </remarks>
		/// <param name="readOnly">Whether to open documents in read only mode, otherwise in read write mode.</param>
		/// <param name="uris">
		/// Uris of documents.
		/// If null or empty, <see cref="GetOpenUri(bool)"/> is used instead and if it returns null, opening is cancelled.
		/// </param>
		/// <exception cref="ArgumentNullException"><paramref name="uris"/> is empty.</exception>
		/// <exception cref="ArgumentException">
		/// Some document/s cannot be open.
		/// All documents that can be open are open even if this exception is thrown.
		/// <see cref="Exception.Data"/> contains a list of actual exceptions as pairs of uri and exception. If an uri is null, its index in <see cref="uris"/> is used instead.
		/// </exception>
		public void Open(bool readOnly, IList<Uri> uris)
		{
			if (uris == null || uris.Count == 0)
				uris = GetOpenUri(ref readOnly);
			if (uris == null)
				return;
			if (uris.Count == 0)
				throw new ArgumentNullException("uris", "No uri specified.");
			ArgumentException exception = new ArgumentException("Open failed.", "uris");
			IDocument document;
			Uri uri;
			for (int i = 0; i < uris.Count; i++)
			{
				uri = uris[i];
				try
				{
					EnsureExists(uri);
					document = this[uri];
					if (document != null)
					{
						// discard already open document and reopen it from uri
						if (document.IsChanged && ShouldReopen(document))
						{
							Remove(document);
						}
						// select document and cancel new opening
						else
						{
							this.Current = document;
							continue;
						}
					}
					document = Open(uri, readOnly);
					document.Uri = uri;
					if (readOnly)
						document.IsReadOnly = readOnly;
					Lock(document, true);
					Add(document);
				}
				catch (Exception e)
				{
					object key = uri;
					if (key == null)
						key = i;
					exception.Data[key] = e;
				}
			}
			if (exception.Data.Count > 0)
				throw exception;
		}
		/// <summary>
		/// Calls <see cref="Open(bool, IList{Uri})"/>.
		/// </summary>
		public void Open(bool readOnly, params Uri[] uris)
		{
			Open(readOnly, uris as IList<Uri>);
		}
		/// <summary>
		/// Calls <see cref="Save(IList{IDocument}, bool)"/> with saveAs false.
		/// </summary>
		public void Save(IList<IDocument> documents)
		{
			Save(documents, false);
		}
		/// <summary>
		/// Calls <see cref="Save(IList{IDocument}, bool)"/> with saveAs false.
		/// </summary>
		public void Save(params IDocument[] documents)
		{
			Save(documents as IList<IDocument>);
		}
		/// <summary>
		/// Calls <see cref="Save(IList{IDocument}, bool)"/> with saveAs true.
		/// </summary>
		public void SaveAs(IList<IDocument> documents)
		{
			Save(documents, true);
		}
		/// <summary>
		/// Calls <see cref="Save(IList{IDocument}, bool)"/> with saveAs true.
		/// </summary>
		public void SaveAs(params IDocument[] documents)
		{
			SaveAs(documents as IList<IDocument>);
		}
		/// <summary>
		/// Calls <see cref="Save(IDocument[])"/> for <see cref="Current"/>.
		/// </summary>
		public void SaveCurrent()
		{
			Save(this.Current);
		}
		/// <summary>
		/// Calls <see cref="SaveAs(IDocument[])"/> for <see cref="Current"/>.
		/// </summary>
		public void SaveCurrentAs()
		{
			SaveAs(this.Current);
		}
		/// <summary>
		/// Saves all documents by calling <see cref="Save(IList{IDocument})"/>(this).
		/// </summary>
		public void SaveAll()
		{
			if (this.Count > 0)
				Save(this);
		}
		/// <summary>
		/// Makes backup of existing document (without deleting it).
		/// Calls <see cref="EnsureExists"/> and <see cref="DoBackup"/> with <see cref="GetBackupUri"/> (first validated with <see cref="Validate(Uri)"/>).
		/// </summary>
		/// <param name="uri">Document uri.</param>
		public void Backup(Uri uri)
		{
			EnsureExists(uri);
			Uri targetUri = GetBackupUri(uri);
			Validate(targetUri);
			DoBackup(uri, targetUri);
		}
		/// <summary>
		/// Closes documents.
		/// </summary>
		/// <remarks>
		/// Gets all changed documents which have <see cref="IDocument.IsChanged"/> true and if at least one such document is found calls <see cref="ShouldSaveChangesBeforeClose"/> and if it returns true, calls <see cref="Save"/> for changed documents. If <see cref="ShouldSaveChangesBeforeClose"/> returns false, does not save anything, if null, cancels closing.
		/// Removes <paramref name="documents"/> from this list.
		/// </remarks>
		/// <param name="documents">Documents.</param>
		/// <returns>Whether documents were closed, otherwise closing was cancelled.</returns>
		/// <exception cref="ArgumentNullException"><paramref name="documents"/> is null or empty.</exception>
		public bool Close(IList<IDocument> documents)
		{
			if (documents == null)
				throw new ArgumentNullException("documents");
			if (documents.Count == 0)
				throw new ArgumentNullException("documents", "No document specified.");
			List<IDocument> changedDocuments = new List<IDocument>(documents.Count);
			foreach (IDocument document in documents)
			{
				if (document != null && document.IsChanged)
					changedDocuments.Add(document);
			}
			if (changedDocuments.Count > 0)
			{
				bool? saveChangedDocuments = ShouldSaveChangesBeforeClose(changedDocuments);
				if (!saveChangedDocuments.HasValue)
					return false;
				else if (saveChangedDocuments.Value && changedDocuments.Count > 0)
					Save(changedDocuments);
			}
			IDocument localDocument;
			for (int i = documents.Count - 1; i >= 0; i--)
			{
				localDocument = documents[i];
				if (!localDocument.IsNew)
					Lock(localDocument, false);
				this.Remove(localDocument);
			}
			return true;
		}
		/// <summary>
		/// Calls <see cref="Close(IList{IDocument})"/>.
		/// </summary>
		public bool Close(params IDocument[] documents)
		{
			return Close(documents as IList<IDocument>);
		}
		/// <summary>
		/// Calls <see cref="Close(IDocument[])"/> for <see cref="Current"/>.
		/// </summary>
		public bool CloseCurrent()
		{
			return Close(this.Current);
		}
		/// <summary>
		/// Closes all documents by calling <see cref="Close(IList{IDocument})"/>(this).
		/// </summary>
		public bool CloseAll()
		{
			if (this.Count > 0)
				return Close(this);
			else
				return true;
		}
		/// <summary>
		/// If <see cref="Current"/> is null, calls <see cref="CloseAll"/>, otherwise calls <see cref="Close(IList{IDocument})"/> for all documents but <see cref="Current"/> if at least one such document exists.
		/// </summary>
		public bool CloseAllButCurrent()
		{
			if (this.Current == null)
			{
				return CloseAll();
			}
			else
			{
				List<IDocument> documents = new List<IDocument>(this.Count - 1);
				foreach (IDocument document in this)
				{
					if (document != this.Current)
						documents.Add(document);
				}
				if (documents.Count > 0)
					return Close(documents);
				else
					return true;
			}
		}
		/// <summary>
		/// Validates <paramref name="uri"/>.
		/// Calls <see cref="DoValidate(Uri)"/>.
		/// </summary>
		/// <param name="uri">Document uri.</param>
		/// <exception cref="ArgumentNullException"><paramref name="uri"/> is null.</exception>
		/// <exception cref="ArgumentException"><paramref name="uri"/> is invalid.</exception>
		public void Validate(Uri uri)
		{
			if (uri == null)
				throw new ArgumentNullException("uri");
			DoValidate(uri);
		}
		/// <summary>
		/// Whether a document with <paramref name="uri"/> is in this list.
		/// Calls <see cref="Validate(Uri)"/>.
		/// </summary>
		/// <param name="uri">Document uri.</param>
		/// <returns>Containment.</returns>
		public bool Contains(Uri uri)
		{
			return this[uri] != null;
		}
		/// <summary>
		/// Whether document at <paramref name="uri"/> already exists.
		/// This deals with actually stored document and not with containment in this list (which <see cref="Contains(Uri)"/> is for).
		/// Calls <see cref="Validate(Uri)"/> and <see cref="DoesExist"/>.
		/// </summary>
		/// <param name="uri">Uri.</param>
		/// <returns>Uri existance.</returns>
		public bool Exists(Uri uri)
		{
			Validate(uri);
			return DoesExist(uri);
		}
		/// <summary>
		/// Gets <paramref name="document"/> name.
		/// </summary>
		/// <param name="document">Document.</param>
		/// <returns>
		/// If <see cref="IDocument.IsNew"/> is false, returns last segment from <see cref="IDocument.Uri"/>.<see cref="Uri.Segments"/>.
		/// Otherwise finds out the number of documents with <see cref="IDocument.IsNew"/> true in this list before this document, adds 1 to the number and formats it to new name (format is got from resources).
		/// </returns>
		/// <exception cref="ArgumentNullException"><paramref name="document"/> is null.</exception>
		public virtual string GetName(IDocument document)
		{
			if (document == null)
				throw new ArgumentNullException("document");
			string name;
			if (document.IsNew)
			{
				int newCount = 0;
				foreach (IDocument localDocument in this)
				{
					if (localDocument == document)
						break;
					if (localDocument.IsNew)
						newCount++;
				}
                name = string.Format(needle.Properties.Resources.Document_DefaultNameFormat, ++newCount);
			}
			else
			{
				name = document.Uri.Segments[document.Uri.Segments.Length - 1];
			}
			return name;
		}

		#region INotifyPropertyChanged Members

		/// <summary>
		/// Fired when a property is changed.
		/// </summary>
		public event PropertyChangedEventHandler PropertyChanged;

		/// <summary>
		/// Fires <see cref="PropertyChanged"/> if not null.
		/// </summary>
		/// <param name="propertyName">Name of changed property.</param>
		protected virtual void OnPropertyChanged(string propertyName)
		{
			if (this.PropertyChanged != null)
				this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
		}

		#endregion

		/// <summary>
		/// Whether call to <see cref="Create"/> makes sense.
		/// </summary>
		/// <value>
		/// <see cref="CreateMethod"/> is not null.
		/// </value>
		protected virtual bool CanCreate
		{
			get { return this.CreateMethod != null; }
		}

		/// <summary>
		/// Creates new document.
		/// </summary>
		/// <remarks>
		/// This implementation calls <see cref="CreateMethod"/> if it is not null, otherwise returns null.
		/// When overriding this method, override <see cref="CanCreate"/> as well.
		/// </remarks>
		/// <returns>New document.</returns>
		protected virtual IDocument DoCreate()
		{
			if (this.CreateMethod != null)
				return this.CreateMethod();
			else
				return null;
		}
		/// <summary>
		/// Opens document.
		/// </summary>
		/// <param name="uri">Valid existing document uri.</param>
		/// <param name="readOnly">Whether to open document in read only mode, otherwise in read write mode.</param>
		/// <returns>Open document.</returns>
		protected abstract IDocument Open(Uri uri, bool readOnly);
		/// <summary>
		/// Saves document.
		/// </summary>
		/// <remarks>
		/// <see cref="IDocument.IsChanged"/> and <see cref="IDocument.Uri"/> is set in <see cref="Save(IList{IDocument}, bool)"/> which calls this method.
		/// </remarks>
		/// <param name="document">Non-null document to be saved.</param>
		/// <param name="uri">Valid uri to save document to. Can be overwritten if exists.</param>
		protected abstract void Save(IDocument document, Uri uri);
		/// <summary>
		/// Makes backup of existing document (without deleting it).
		/// Called by <see cref="Backup"/>.
		/// </summary>
		/// <param name="sourceUri">Existing document source uri.</param>
		/// <param name="targetUri">Backup document target uri.</param>
		protected abstract void DoBackup(Uri sourceUri, Uri targetUri);
		/// <summary>
		/// Gets new uri for opening documents.
		/// </summary>
		/// <remarks>
		/// This implementation calls <see cref="GetOpenUriMethod"/> if it is not null, otherwise returns null.
		/// </remarks>
		/// <param name="readOnly">Whether to use read only open mode. This value can be changed.</param>
		/// <returns>New uris. null if opening is cancelled.</returns>
		protected virtual IList<Uri> GetOpenUri(ref bool readOnly)
		{
			if (this.GetOpenUriMethod != null)
				return this.GetOpenUriMethod(ref readOnly);
			else
				return null;
		}
		/// <summary>
		/// Gets new uri for saving document to this new uri.
		/// </summary>
		/// <remarks>
		/// This implementation calls <see cref="GetSaveUriMethod"/> if it is not null, otherwise returns null.
		/// </remarks>
		/// <param name="document">Document to be saved.</param>
		/// <returns>New uri. null if saving is cancelled.</returns>
		protected virtual Uri GetSaveUri(IDocument document)
		{
			if (this.GetSaveUriMethod != null)
				return this.GetSaveUriMethod(document);
			else
				return null;
		}
		/// <summary>
		/// Creates new document backup target uri from <paramref name="uri"/>.
		/// </summary>
		/// <param name="uri">Source document uri.</param>
		/// <returns>
		/// Backup uri.
		/// New uri from "<paramref name="uri"/>.<see cref="Uri.AbsoluteUri"/> + <see cref="BackupExtension"/>".
		/// </returns>
		protected virtual Uri GetBackupUri(Uri uri)
		{
			return new Uri(uri.AbsoluteUri + this.BackupExtension);
		}
		/// <summary>
		/// Whether document at <paramref name="uri"/> already exists.
		/// Called by <see cref="Exists(Uri)"/>.
		/// </summary>
		/// <param name="uri">Valid document uri.</param>
		/// <returns>Uri existance.</returns>
		protected abstract bool DoesExist(Uri uri);
		/// <summary>
		/// Whether <paramref name="document"/> should be reopen and its current changes discarded.
		/// </summary>
		/// <remarks>
		/// This implementation calls <see cref="ShouldReopenMethod"/> if it is not null, otherwise returns false.
		/// </remarks>
		/// <param name="document">Already open document.</param>
		/// <returns>
		/// true to reopen <paramref name="document"/>.
		/// false to set <see cref="Current"/> to <paramref name="document"/>.
		/// </returns>
		protected virtual bool ShouldReopen(IDocument document)
		{
			if (this.ShouldReopenMethod != null)
				return this.ShouldReopenMethod(document);
			else
				return false;
		}
		/// <summary>
		/// Whether <paramref name="documents"/> changes should be saved before closing.
		/// </summary>
		/// <remarks>
		/// This implementation calls <see cref="ShouldSaveChangesBeforeCloseMethod"/> if it is not null, otherwise returns true.
		/// </remarks>
		/// <param name="documents">
		/// Changed documents.
		/// Documents which should not be saved can be removed from this list.
		/// </param>
		/// <returns>
		/// true to save documents.
		/// false not to save documents.
		/// null to cancel documents closing.
		/// </returns>
		protected virtual bool? ShouldSaveChangesBeforeClose(IList<IDocument> documents)
		{
			if (this.ShouldSaveChangesBeforeCloseMethod != null)
				return this.ShouldSaveChangesBeforeCloseMethod(documents);
			else
				return true;
		}
		/// <summary>
		/// Whether already existing document at <paramref name="uri"/> (<see cref="Exists"/> is true) should be overwritten by saving <paramref name="document"/>.
		/// </summary>
		/// <remarks>
		/// This implementation calls <see cref="ShouldOverwriteMethod"/> if it is not null, otherwise returns true.
		/// </remarks>
		/// <param name="document">Document to be saved.</param>
		/// <param name="uri">Uri of <paramref name="document"/>.</param>
		/// <returns>
		/// true to overwrite document.
		/// false not to overwrite document and choose new uri.
		/// null to cancel document saving.
		/// </returns>
		protected virtual bool? ShouldOverwrite(IDocument document, Uri uri)
		{
			if (this.ShouldOverwriteMethod != null)
				return this.ShouldOverwriteMethod(document, uri);
			else
				return true;
		}
		/// <summary>
		/// Whether "document" at <paramref name="uri"/> should be backed up before it is overwritten.
		/// </summary>
		/// <remarks>
		/// This implementation calls <see cref="ShouldBackupMethod"/> if it is not null, otherwise returns true.
		/// </remarks>
		/// <param name="uri">Existing document uri.</param>
		/// <returns>Beckup.</returns>
		protected virtual bool ShouldBackup(Uri uri)
		{
			if (this.ShouldBackupMethod != null)
				return this.ShouldBackupMethod(uri);
			else
				return true;
		}
		/// <summary>
		/// Validates <paramref name="uri"/>.
		/// </summary>
		/// <remarks>This implementation does nothing.</remarks>
		/// <param name="uri">Non-nul uri.</param>
		/// <exception cref="ArgumentException"><paramref name="uri"/> is invalid.</exception>
		protected virtual void DoValidate(Uri uri)
		{
		}
		/// <summary>
		/// Un/Locks <paramref name="document"/>.
		/// </summary>
		/// <remarks>This implementation does nothing.</remarks>
		/// <param name="document">Document which is not new (<see cref="IDocument.IsNew"/> is false).</param>
		/// <param name="doLock">
		/// If true, lock <paramref name="document"/>.
		/// If false, unlock <paramref name="document"/>.
		/// </param>
		protected virtual void Lock(object document, bool doLock)
		{
		}

		/// <summary>
		/// Sets <see cref="Current"/>.
		/// </summary>
		/// <remarks>
		/// If <paramref name="document"/> is not null and <see cref="Contains"/> is true, <see cref="Current"/> is set to it.
		/// Otherwise if at least one document exists in this list, <see cref="Current"/> is set to the first document, otherwise to null.
		/// </remarks>
		/// <param name="document">Document.</param>
		protected void SetCurrentDocument(IDocument document)
		{
			if (document != null && Contains(document))
			{
				this.Current = document;
			}
			else
			{
				if (this.Count > 0)
					this.Current = this[0];
				else
					this.Current = null;
			}
		}
		/// <summary>
		/// Saves documents.
		/// </summary>
		/// <remarks>
		/// When a document has <see cref="IDocument.IsNew"/> or <see cref="IDocument.IsReadOnly"/> true, it is saved in "save as" mode regardless of <paramref name="saveAs"/> value.
		/// If "save as" mode is not used:
		/// Documents with <see cref="IDocument.IsChanged"/> false are skipped.
		/// If "save as" mode is used:
		/// 1. New uri is got from <see cref="GetSaveUri(IDocument)"/>. If it is null, saving is skipped.
		/// 2. If <see cref="Exists"/> returns true for the new uri, <see cref="ShouldOverwrite"/> is called and if it returns true, saving continues, if false, operations are repeated from point 1., if null, saving is skipped.
		/// 3. If existing file should be overwritten <see cref="ShouldBackup"/> is called and if it returns true, <see cref="Backup"/> is called.
		/// Calls <see cref="Save(IDocument, Uri)"/> for each document.
		/// </remarks>
		/// <param name="documents">Documents.</param>
		/// <param name="saveAs">Whether to do "save as" to new uri otherwise normal save to current uri.</param>
		/// <exception cref="ArgumentNullException"><paramref name="documents"/> is null or empty.</exception>
		/// <exception cref="ArgumentException">
		/// Some document/s cannot be saved.
		/// All documents that can be saved are open saved if this exception is thrown.
		/// <see cref="Exception.Data"/> contains a list of actual exceptions as pairs of document and exception. If a document is null, its index in <see cref="documents"/> is used instead.
		/// </exception>
		protected void Save(IList<IDocument> documents, bool saveAs)
		{
			if (documents == null)
				throw new ArgumentNullException("documents");
			if (documents.Count == 0)
				throw new ArgumentNullException("documents", "No document specified.");
			ArgumentException exception = new ArgumentException("Save failed.", "uris");
			Uri uri;
			IDocument document;
			bool saveAsNew;
			bool? shouldOverwrite;
			for (int i = 0; i < documents.Count; i++)
			{
				document = documents[i];
				try
				{
					if (document == null)
						throw new ArgumentNullException("document");
					uri = document.Uri;
					saveAsNew = saveAs || document.IsNew || document.IsReadOnly;
					if (!saveAsNew && !document.IsChanged)
						continue;
					if (saveAsNew)
					{
					CheckNewUri:
						uri = GetSaveUri(document);
						if (uri == null)
							continue;
						if (Exists(uri))
						{
							shouldOverwrite = ShouldOverwrite(document, uri);
							// over write
							if (shouldOverwrite == true && ShouldBackup(uri))
								Backup(uri);
							// get new uri and check it again
							if (shouldOverwrite == false)
								goto CheckNewUri;
							// cancel
							if (shouldOverwrite == null)
								continue;
						}
					}
					Validate(uri);
					Save(document, uri);
					document.Uri = uri;
					document.IsChanged = false;
				}
				catch (Exception e)
				{
					object key = document;
					if (key == null)
						key = i;
					exception.Data[key] = e;
				}
			}
			if (exception.Data.Count > 0)
				throw exception;
		}
		/// <summary>
		/// Ensures document at <paramref name="uri"/> exists.
		/// Calls <see cref="Validate{Uri)"/> and <see cref="Exists(Uri)"/>.
		/// </summary>
		/// <param name="uri">Document uri.</param>
		/// <exception cref="ArgumentException"><see cref="Exists(Uri)"/> returned false.</exception>
		protected void EnsureExists(Uri uri)
		{
			Validate(uri);
			if (!Exists(uri))
				throw new ArgumentException("Document at uri does not exist.", "uri");
		}

		/// <summary>
		/// When this list changes, sets <see cref="Current"/> through <see cref="SetCurrentDocument"/> and sets <see cref="IDocument.Parent"/> to this instance.
		/// </summary>
		/// <param name="e"></param>
		protected override void OnItemsChange(ListItemsChangeEventArgs<IDocument> e)
		{
			IDocument currentDocument = this.Current;
			switch (e.Change)
			{
				case ListItemsChanges.Add:
					currentDocument = e.NewItem;
					e.NewItem.Parent = this;
					break;
				case ListItemsChanges.Remove:
					if (this.Count > 0 && e.OldIndex <= this.Count)
						currentDocument = this[e.OldIndex - 1];
					break;
				case ListItemsChanges.Replace:
					currentDocument = e.NewItem;
					e.NewItem.Parent = this;
					break;
				case ListItemsChanges.Reset:
					foreach (IDocument document in this)
					{
						document.Parent = this;
					}
					break;
			}
			SetCurrentDocument(currentDocument);
			base.OnItemsChange(e);
		}

		/// <summary>
		/// See <see cref="Current"/>.
		/// </summary>
		private IDocument _current;
		/// <summary>
		/// See <see cref="BackupExtension"/>.
		/// </summary>
		private string _backupExtension = ".bak";
	}
}
